#-*- codeing = utf-8 -*-
#@Time : 2020/11/25 21:10
#@Author : 阳某
#@File : 20.socket_server.py
#@Software : PyCharm


'''
示例1 中，我们给出的参考 接口， 传递的消息都是 放在一个大字符串里面， 然后采用字符串编码为 字节串进行传输的。
这种接口设计的好处就是简单，便于发送时的字节编码操作：消息头和消息体分别进行 UTF8编码，然后字节串拼接即可
接收方处理也简单，直接分离出消息头和消息体，分别进行UTF8解码即可。
我们设计普通应用程序之间的通信，这样就很好，简单就是美，容易开发，容易维护。
但是如果消息接口是在 秒理万机 的计算节点之间的通讯， 这样的接口的缺点就暴露了：消息长，而且编解码耗费处理器资源比较大。
典型的例子，就是通讯设备， 比如 4G核心网的业务处理节点。 它们每秒往往要处理数以万计的认证、鉴权、计费 等消息，采用上述方法会给设备带来巨大负担。
首先，数据都用字符表示，其实是比较浪费带宽的做法。
比如返回码 用 200 这样的字符串表示，就会耗费3个字节，24个比特。 如果处理结果 只有成功和不成功，只需要1个bit 即可， 1表示成功，0表示不成功
其次， json这种复杂语法的编解码算法，需要程序代码进行各种复杂处理（参考一下Python json内置库的代码）是比较耗费CPU 资源的。
可以定义更为简单的数据表达方式，比如像这样：
消息头 开头2个字节表示消息的长度
消息头 第3个字节表示 消息类型 ：

0：暂停命令， 1 ：恢复命令 2：命令响应 3：统计上报 4：统计上报响应
消息体 数据定义

可以使用类似 Radius/Diameter Attribute-Value Pairs （AVP） 的定义方法

Attribute： 使用一个字节，表示数据种类。

比如

1： CPU 使用率 2： 内存使用率

Length： 使用一个字节，表示信息长度

Value： 表示具体的数据值

这样，前面的示例信息

 {
        "CPU Usage" : "30%",
        "Mem usage" : "53%"
 }
其中

"CPU Usage" : "30%" , 就像这样用16进制字节表示 01011E

"Mem usage" : "53%" , 就像这样用16进制字节表示 020135

合起来就是 01011E020135

对比一下，第一种编码方法

优点：更节省传输带宽，编码解码数据效率更高
缺点：对于人的可读性差，数据表示灵活性较差；
支持多个客户
上面的服务端代码 只能和一个客户端进行通信。
如果我们同时运行多个客户端，就会发现 后面的客户端程序不能和服务端连接成功。为什么呢？
因为，服务端程序必须不停的对 监听 socket 对象调用 accept()方法，才能不断的接受 新的客户端连接请求。
而且 还需要运行额外的代码 对 多个客户端连接后，返回的多个数据传输socket对象 进行数据的收发。
显然，我们上面的程序没有这样的处理。
因为缺省情况创建的 socket 是 阻塞式 的，进行 accpet调用时，如果没有客户端连接，程序就阻塞在此处，不再执行后续代码。
同样的，调用recv方法，如果没有数据在本socket的接收缓冲，也会阻塞。
所以，通常一个线程里面，没法 不断地 调用 监听socket的 accept方法，同时还能 负责多个 数据传输socket消息的收发。
那么让一个服务端程序 和多个客户端同时连接 并 通信 呢？
聪明的你一定想到了，一个线程不行，就使用多个线程啊。
'''

# 课程链接
# https://www.bilibili.com/video/BV1a7411z75u?p=5
#  === TCP 服务端程序 server.py ， 支持多客户端 ===

# 导入socket 库
from socket import *
from threading import Thread

IP = ''
PORT = 50000
BUFLEN = 512

# 这是新线程执行的函数，每个线程负责和一个客户端进行通信
def clientHandler(dataSocket,addr):
    while True:
        recved = dataSocket.recv(BUFLEN)
        # 当对方关闭连接的时候，返回空字符串
        if not recved:
            print(f'客户端{addr} 关闭了连接' )
            break

        # 读取的字节数据是bytes类型，需要解码为字符串
        info = recved.decode()
        print(f'收到{addr}信息： {info}')

        dataSocket.send(f'服务端接收到了信息 {info}'.encode())

    dataSocket.close()

# 实例化一个socket对象 用来监听客户端连接请求
listenSocket = socket(AF_INET, SOCK_STREAM)

# socket绑定地址和端口
listenSocket.bind((IP, PORT))

listenSocket.listen(8)
print(f'服务端启动成功，在{PORT}端口等待客户端连接...')

while True:
   # 在循环中，一直接受新的连接请求
   dataSocket, addr = listenSocket.accept()     # Establish connection with client.
   addr = str(addr)
   print(f'一个客户端 {addr} 连接成功' )

   # 创建新线程处理和这个客户端的消息收发
   th = Thread(target=clientHandler,args=(dataSocket,addr))
   th.start()

listenSocket.close()


'''
多线程方式有个缺点。

如果一个服务端要同时处理大量的客户端连接，比如10000个，需要创建10000个线程。

而操作系统通常不可能为一个进程分配这么多的线程。

实际上，我们的服务端程序，大部分时间都是空闲的，都在等待连接请求，等待接受消息，根本不需要这么多的线程来处理。

这种程序通常被称之为 IO bound 程序，也就是说程序的主要时间都是花费在 IO 上面。

这种程序，其实一个线程就足够了。

关键问题是，需要这一个线程 很好的分配 时间， 在有连接请求到来的时候，执行处理连接请求代码，有消息到达socket缓冲的时候，执行读取处理消息的代码。

这种处理方式称之为异步IO。

Python 3 新增了 asyncio 库， 我们可以使用该库来 实现 同时处理多个客户端数据收发。
'''

#  === TCP 服务端程序 server.py 异步支持多客户端 ===
import asyncio, socket
IP = ''
PORT = 50000
BUFLEN = 512

# 定义处理数据收发的回调
async def handle_echo(reader, writer):
    addr = writer.get_extra_info('peername')
    while True:
        data = await reader.read(100)
        if not data:
            print(f'客户端{addr}关闭了连接')
            writer.close()
            break

        message = data.decode()
        print(f'收到{addr}信息： {message}')

        writer.write(data)

loop = asyncio.get_event_loop()
coro = asyncio.start_server(handle_echo, IP, PORT, loop=loop)
server = loop.run_until_complete(coro)

# Serve requests until Ctrl+C is pressed
print('服务端启动成功，在{}端口等待客户端连接...'.format(server.sockets[0].getsockname()[1]))
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
